package com.ocm.core.service.impl;

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.DataAccessException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.ocm.core.dao.SecurityDao;
import com.ocm.core.domain.Permission;
import com.ocm.core.domain.Security;
import com.ocm.core.domain.Security.SecurityElement;
import com.ocm.core.domain.Security.SecurityElements;
import com.ocm.core.domain.SecurityType;
import com.ocm.core.domain.UserDetail;
import com.ocm.core.domain.UserDetailType;
import com.ocm.core.dto.GroupDto;
import com.ocm.core.dto.UserInfoDto;
import com.ocm.core.exception.InsufficentPermissionException;
import com.ocm.core.exception.OCMRuntimeException;
import com.ocm.core.exception.UnsupportTypeException;
import com.ocm.core.mapping.DTOService;
import com.ocm.core.mapping.DomainService;
import com.ocm.core.service.SecurityService;
import com.ocm.core.util.OCMUtil;
import com.opm.security.core.model.ExternalUserDetails;
import com.opm.security.core.model.ExternalUserDetailsImpl;
import com.opm.security.core.service.UserInfoManager;
import com.opm.security.core.util.UserUtil;

@Service("securityService")
@Transactional(readOnly = true)
public class SecurityServiceImpl implements SecurityService {

   private SecurityDao securityDao;

   private DomainService domainService;

   private DTOService dtoService;

   private UserInfoManager userInfoManager;

   private ExternalUserDetails anonymousUserInfo;

   @Autowired
   public SecurityServiceImpl(
         @Qualifier("securityDao")SecurityDao securityDao,
         @Qualifier("domainService")DomainService domainService,
         @Qualifier("userInfoManager")UserInfoManager userInfoManager,
         @Qualifier("dtoService") DTOService dtoService) {
      this.securityDao = securityDao;
      this.userInfoManager = userInfoManager;
      this.domainService = domainService;
      this.dtoService = dtoService;
   }

   @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
   public void saveGroup(GroupDto groupDto) {
      UserDetail group = this.domainService.convertGroup(groupDto);
      group.setType(UserDetailType.GROUP);
      group.setIsCredentialsNonExpired(true);
      this.securityDao.save(group);
   }

   @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
   public Long addUser(UserInfoDto userInfoDto) {
      List<GroupDto> groupDtos = userInfoDto.getUserDetails();
      String defaultAuthorities = SecurityType.USER.getValue();
      if (groupDtos != null && groupDtos.size() > 0){
         defaultAuthorities = OCMUtil.getAuthorities(groupDtos);
      }
      userInfoDto.setAuthority(defaultAuthorities);
      UserDetail userDetail = this.domainService.convertUserInfo(userInfoDto);
      userDetail.setType(UserDetailType.USER);
      userDetail.setIsCredentialsNonExpired(true);
      this.securityDao.save(userDetail);
      return userDetail.getId();
   }

   public List<GroupDto> findAll() {
      List<UserDetail> groups = this.securityDao.findGroupAll();
      List<GroupDto> groupDtos = this.dtoService.convertGroupDtos(groups, null);
      return groupDtos;
   }

   public boolean hasExistGroupName(String groupName) {
      UserDetail group = this.securityDao.findByGroupName(groupName);
      if (group != null)
         return true;
      return false;
   }

   public List<GroupDto> findGroups() {
      List<UserDetail> userDetails = this.securityDao.findGroupAll();
      List<GroupDto> groupDtos = this.dtoService.convertGroupDtos(userDetails, null);
      return groupDtos;
   }

   public List<GroupDto> findGroups(Long[] groupIds) {
      List<UserDetail> userDetails = this.securityDao.findAll(groupIds);
      List<GroupDto> groupDtos = this.dtoService.convertGroupDtos(userDetails, null);
      return groupDtos;
   }

   public GroupDto findByGroupId(long groupId) {
      UserDetail groupDetail = this.securityDao.findById(groupId);
      GroupDto groupDto = this.dtoService.convertGroupDto(groupDetail, null);
      return groupDto;
   }

   public List<UserInfoDto> findAllUser() {
      List<UserDetail> userInfos = this.securityDao.findUserAll();
      List<UserInfoDto> userInfoDtos = this.dtoService.convertUserDtos(userInfos, null);
      return userInfoDtos;
   }

   public boolean hasExistUserName(String userName) {
      UserDetail userDetails = (UserDetail) this.securityDao.findByUsername(userName);
      if (userDetails != null)
         return true;
      return false;
   }

   public UserInfoDto findUserInfoByUsername(String userName) {
      UserDetail userDetail = (UserDetail) this.securityDao.findByUsername(userName);
      UserInfoDto userDto = this.dtoService.convertUserDto(userDetail, null);
      return userDto;
   }
   
   public List<UserInfoDto> findAll(Long[] secIds) {
      List<UserDetail> userDetails = this.securityDao.findAll(secIds);
      List<UserInfoDto> userDtos = this.dtoService.convertUserDtos(userDetails, null);
      return userDtos;
   }

   public UserInfoDto findById(long id) {
      UserDetail userDetail = this.securityDao.findById(id);
      UserInfoDto userDto = this.dtoService.convertUserDto(userDetail, null);
      return userDto;
   }

   public UserInfoDto findByUserId(long userId) {
      UserDetail result = this.securityDao.findById(userId);
      UserInfoDto userInfoDto = this.dtoService.convertUserDto(result, null);
      return userInfoDto;
   }

   public GroupDto findByGroupname(String groupname) {
      UserDetail result = (UserDetail) this.securityDao.findByGroupName(groupname);
      GroupDto groupDto = this.dtoService.convertGroupDto(result, null);
      return groupDto;
   }

   public UserInfoDto findUserInfoByGroupname(String groupname) {
      UserDetail result = (UserDetail) this.securityDao.findByGroupName(groupname);
      UserInfoDto userInfoDto = this.dtoService.convertUserDto(result, null);
      return userInfoDto;
   }

   public List<GroupDto> findByGroupnames(String[] groupnames) {
      List<UserDetail> groups = this.securityDao.findListByGroupNames(groupnames);
      List<GroupDto> groupDtos = this.dtoService.convertGroupDtos(groups, null);
      return groupDtos;
   }

   public List<UserInfoDto> searchByName(String name) {
      List<UserDetail> userInfos = this.securityDao.findListByName(name);
      List<UserInfoDto> userInfoDtos = this.dtoService.convertUserDtos(userInfos, null);
      return userInfoDtos;
   }

   public List<UserInfoDto> searchByUserName(String userName) {
      List<UserDetail> userInfos = this.securityDao.findListByUsername(userName);
      List<UserInfoDto> userInfoDtos = this.dtoService.convertUserDtos(userInfos, null);
      return userInfoDtos;
   }

   public List<GroupDto> searchByGroupName(String groupName) {
      List<UserDetail> groups = this.securityDao.findListByGroupName(groupName);
      List<GroupDto> groupDtos = this.dtoService.convertGroupDtos(groups, null);
      return groupDtos;
   }

   public UserInfoDto getCurrentUsername() {
      UserInfoDto userInfoDto = (UserInfoDto) this.getAuthenticatedUser().getExtUserDetails();
      return userInfoDto;
   }

   public boolean hasPermission(Security security, Permission permission) {
      try {
         UserInfoDto result = (UserInfoDto)  this.getAuthenticatedUser().getExtUserDetails();
         long currentUserId = result.getId();
         Collection<GrantedAuthority> grantedAuthorities = result.getAuthorities();
         for (Iterator<GrantedAuthority> it= grantedAuthorities.iterator(); it.hasNext();){
            GrantedAuthority grantedAuthority = it.next();
            if (SecurityType.SYSADMIN.getValue().equalsIgnoreCase(grantedAuthority.getAuthority())){
               return true;
            }
         }
         if (security == null){
            return false;
         }
         Map<Long, SecurityElement> secGroups = new HashMap<Long, SecurityElement>();
         SecurityElement anonymous = null;
         Permission[] permissions = null;
         SecurityElements securityElements = security.getSecurityElements();
         while (securityElements.hasNext()){
            SecurityElement securityElement = securityElements.next();
            if ((securityElement.getHasUser() == true || securityElement.getHasCreator() == true) && currentUserId == securityElement.getUserId().longValue()){
               permissions = Permission.getPermissions(securityElement.getAccessMask());
               return hasPermission(permission, permissions);
            }else if (securityElement.getHasAnonymous() == true){
               anonymous = securityElement;
            }else if (securityElement.getHasGroup() == true){
               secGroups.put(securityElement.getUserId(), securityElement);
            }
         }
            
         List<UserDetail> userDetails = this.securityDao.findAll(secGroups.keySet().toArray(new Long[]{}), currentUserId);
         if (userDetails.size() > 0){
            for (UserDetail user : userDetails){
               SecurityElement securityElement = secGroups.get(user.getId());
               permissions = Permission.getPermissions(securityElement.getAccessMask());
               if (hasPermission(permission, permissions)){
                  return true;
               }
            }
         }else if (anonymous != null){
            permissions = Permission.getPermissions(anonymous.getAccessMask());
            return hasPermission(permission, permissions);
         }
      }catch (UnsupportTypeException e){
         throw new OCMRuntimeException(e);
      }
      return false;
   }

   public void verifyPermission (Security security, Permission permission){
      if (!hasPermission(security, permission))
         throw new InsufficentPermissionException("UNAuthenticated, Do not allow permissions!");
   }
   
   private boolean hasPermission(Permission permission, Permission[] permissions){
      for (Permission per : permissions){
         if (per == permission){
            return true;
         }
      }
      return false;
   }
   
   private ExternalUserDetails getAuthenticatedUser() {
      SecurityContext securityContext = SecurityContextHolder.getContext();
      if (securityContext != null && securityContext.getAuthentication() != null) {
         Object principal = securityContext.getAuthentication().getPrincipal();
         if (principal instanceof UserDetails){
            return (ExternalUserDetails) principal;
         } else if (principal instanceof String && this.userInfoManager.isAllowedAnonymousUser() && UserUtil.getGuestUsername().equalsIgnoreCase(principal.toString())){
            if (anonymousUserInfo == null){
               anonymousUserInfo = (ExternalUserDetails) this.loadUserByUsername(UserUtil.getGuestUsername());
            }
            if (anonymousUserInfo != null){
               return anonymousUserInfo;
            }
         }
      }
      throw new SecurityException("User authentication is not available");
   }

   public ExternalUserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
      UserDetail userDetail = this.securityDao.findByUsername(username);
      ExternalUserDetails externalUserDetails = null;
      if (userDetail != null){
         UserInfoDto userDto = this.dtoService.convertUserDto(userDetail, null);
         externalUserDetails = new ExternalUserDetailsImpl();
         externalUserDetails.setExtUserDetails(userDto);
      }
      return externalUserDetails;
   }
}
